tsconfig.json의 include에 프로젝트 루트 경로의 파일을 추가하면 rootDir이 변경될 수 있음 {troubleshooting}
PR 머지 후 갑자기 발생한 도커 배포 문제
아래 에러 로그에 따르면 도커 컨테이너가 /app/dist/main
을 찾을 수 없다는데..
racketime-api on dev [$] via ⬢ v22.11.0 on 🐳 v27.5.1 took 3.7s
➜ docker-compose -f infra/docker-compose.racket-time-api.build.yml up --build
WARN[0000] /Users/choiwheatley/workspace/racketime-api/infra/docker-compose.racket-time-api.build.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion
[+] Building 39.1s (13/13) FINISHED docker:desktop-linux
=> [app internal] load build definition from Dockerfile.prod 0.0s
=> => transferring dockerfile: 652B 0.0s
=> WARN: FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 1) 0.0s
=> WARN: FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 12) 0.0s
=> [app internal] load metadata for docker.io/library/node:lts 0.9s
=> [app internal] load .dockerignore 0.0s
=> => transferring context: 112B 0.0s
=> [app internal] load build context 0.1s
=> => transferring context: 1.16MB 0.1s
=> [app builder 1/4] FROM docker.io/library/node:lts@sha256:f6b9c31ace05502dd98ef777aaa20464362435dcc5e312b0e213121dcf7d8b95 0.0s
=> => resolve docker.io/library/node:lts@sha256:f6b9c31ace05502dd98ef777aaa20464362435dcc5e312b0e213121dcf7d8b95 0.0s
=> CACHED [app builder 2/4] WORKDIR /app 0.0s
=> [app builder 3/4] COPY . /app 0.2s
=> [app builder 4/4] RUN npm i -g pnpm && pnpm install && npx prisma generate && pnpm run build 20.5s
=> CACHED [app prod 3/5] RUN apt-get update && apt-get install -y curl 0.0s
=> [app prod 4/5] COPY --chown=node:node --from=builder /app /app 2.2s
=> [app prod 5/5] RUN chmod +x /app/infra/entrypoint.sh 0.3s
=> [app] exporting to image 12.8s
=> => exporting layers 9.8s
=> => exporting manifest sha256:e865d50bb6d0e58795de4cc2f259943fdee1c8c7c844f10a31a65f9c5bacc30b 0.0s
=> => exporting config sha256:cc2bb129c7424ccd0885f910fc1db13907d34293e98ec2aa175c6952ad30d29e 0.0s
=> => exporting attestation manifest sha256:bd6dc726a0cfa611619938f22e2666b9c36cfbac25679306daaa94d838c30b61 0.0s
=> => exporting manifest list sha256:82cefa417aa2828567d7e9f563b055c7e9d9f001cee598649adbf32e23263d8d 0.0s
=> => naming to docker.io/library/infra-app:latest 0.0s
=> => unpacking to docker.io/library/infra-app:latest 3.0s
=> [app] resolving provenance for metadata file 0.0s
[+] Running 2/2
✔ app Built 0.0s
✔ Container infra-app-1 Recreated 1.0s
Attaching to app-1
app-1 | node:internal/modules/cjs/loader:1228
app-1 | throw err;
app-1 | ^
app-1 |
app-1 | Error: Cannot find module '/app/dist/main'
app-1 | at Function._resolveFilename (node:internal/modules/cjs/loader:1225:15)
app-1 | at Function._load (node:internal/modules/cjs/loader:1055:27)
app-1 | at TracingChannel.traceSync (node:diagnostics_channel:322:14)
app-1 | at wrapModuleLoad (node:internal/modules/cjs/loader:220:24)
app-1 | at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:170:5)
app-1 | at node:internal/main/run_main_module:36:49 {
app-1 | code: 'MODULE_NOT_FOUND',
app-1 | requireStack: []
app-1 | }
app-1 |
app-1 | Node.js v22.14.0
app-1 exited with code 1
main 브랜치에서 똑같은 걸 하면..
잘 된다. 이건 머지하면서 뭐가 잘못 꼬인거다.
~/Downloads/app-b8fc19-250225_015236436 via ⬢ v22.11.0 on 🐳 v27.5.1
➜ docker-compose -f infra/docker-compose.racket-time-api.build.yml up --build
WARN[0000] /Users/choiwheatley/Downloads/app-b8fc19-250225_015236436/infra/docker-compose.racket-time-api.build.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion
[+] Building 40.2s (14/14) FINISHED docker:desktop-linux
=> [app internal] load build definition from Dockerfile.prod 0.0s
=> => transferring dockerfile: 698B 0.0s
=> WARN: FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 1) 0.0s
=> WARN: FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 12) 0.0s
=> [app internal] load metadata for docker.io/library/node:lts 1.8s
=> [app auth] library/node:pull token for registry-1.docker.io 0.0s
=> [app internal] load .dockerignore 0.0s
=> => transferring context: 158B 0.0s
=> [app internal] load build context 0.1s
=> => transferring context: 239.31kB 0.0s
=> [app builder 1/4] FROM docker.io/library/node:lts@sha256:f6b9c31ace05502dd98ef777aaa20464362435dcc5e312b0e213121dcf7d8b95 0.0s
=> => resolve docker.io/library/node:lts@sha256:f6b9c31ace05502dd98ef777aaa20464362435dcc5e312b0e213121dcf7d8b95 0.0s
=> CACHED [app builder 2/4] WORKDIR /app 0.0s
=> [app builder 3/4] COPY . /app 0.1s
=> [app builder 4/4] RUN npm i -g pnpm && pnpm install && npx prisma generate && pnpm run build 22.6s
=> CACHED [app prod 3/5] RUN apt-get update && apt-get install -y curl 0.0s
=> [app prod 4/5] COPY --chown=node:node --from=builder /app /app 1.7s
=> [app prod 5/5] RUN chmod +x /app/infra/entrypoint.sh 0.3s
=> [app] exporting to image 11.8s
=> => exporting layers 9.0s
=> => exporting manifest sha256:33099e4489f7619905bd1e24cb3549dc57718342128016c9b4bfdf6bccfe6a6e 0.0s
=> => exporting config sha256:bebde6a9a02c95499840535f932d05fb0240be070bc33a27866062232662af3e 0.0s
=> => exporting attestation manifest sha256:4f8247d773818dc9ab6ce5364dec9d6c2ea080ebf53d6f6640ec62baa12fb95f 0.0s
=> => exporting manifest list sha256:dea7e14826f30de52ceff975b5b3fcf95608913036016733d4dfcc0036210eda 0.0s
=> => naming to docker.io/library/infra-app:latest 0.0s
=> => unpacking to docker.io/library/infra-app:latest 2.9s
=> [app] resolving provenance for metadata file 0.0s
[+] Running 2/2
✔ app Built 0.0s
✔ Container infra-app-1 Recreated 1.5s
Attaching to app-1
app-1 | [Nest] 7 - 03/13/2025, 7:43:09 AM LOG [NestFactory] Starting Nest application...
app-1 | [Nest] 7 - 03/13/2025, 7:43:09 AM LOG [InstanceLoader] VerificationModule dependencies initialized +16ms
app-1 | [Nest] 7 - 03/13/2025, 7:43:09 AM LOG [InstanceLoader] ConfigHostModule dependencies initialized +0ms
app-1 | [Nest] 7 - 03/13/2025, 7:43:09 AM LOG [InstanceLoader] ProductLogModule dependencies initialized +0ms
app-1 | [Nest] 7 - 03/13/2025, 7:43:09 AM LOG [InstanceLoader] ConfigModule dependencies initialized +1ms
app-1 | [Nest] 7 - 03/13/2025, 7:43:09 AM LOG [InstanceLoader] AppModule dependencies initialized +0ms
main, dev 빌드 이후 dist 디렉터리 내용물도 차이가 크다.
dev
얘는 src 안에 main.js가 들어갔는데? 어찌 된 일이지?
racketime-api/dist
➜ l
total 1056
drwxr-xr-x@ 7 choiwheatley staff 224 Mar 13 16:36 ./
drwxr-xr-x@ 31 choiwheatley staff 992 Mar 13 16:43 ../
-rw-r--r--@ 1 choiwheatley staff 108 Mar 13 16:36 jest.config.d.ts
-rw-r--r--@ 1 choiwheatley staff 676 Mar 13 16:36 jest.config.js
-rw-r--r--@ 1 choiwheatley staff 515 Mar 13 16:36 jest.config.js.map
drwxr-xr-x@ 45 choiwheatley staff 1440 Mar 13 16:36 src/
-rw-r--r--@ 1 choiwheatley staff 527286 Mar 13 16:36 tsconfig.build.tsbuildinfo
➜ ls src
academy app.module.d.ts coach-file gamedatalog metrix reservation-code tag verification
academy-bot-scheduler app.module.js constant.d.ts guidance order settlement tennis-content
academy-coach app.module.js.map constant.js health payment shared ticket
academy-tag auth constant.js.map main.d.ts product-log sita trainer-ota
admin business-info court main.js product-order staff types
advertisement coach-code fb main.js.map reservation switch-bot user
main
Downloads/app-b8fc19-250225_015236436/dist
➜ l
total 1104
drwxr-xr-x@ 47 choiwheatley staff 1504 Mar 13 16:38 ./
drwx------@ 26 choiwheatley staff 832 Mar 13 16:40 ../
drwxr-xr-x@ 19 choiwheatley staff 608 Mar 13 16:38 academy/
...
-rw-r--r--@ 1 choiwheatley staff 3008 Mar 13 16:38 main.js
...
drwxr-xr-x@ 15 choiwheatley staff 480 Mar 13 16:38 user/
drwxr-xr-x@ 9 choiwheatley staff 288 Mar 13 16:38 verification/
dev는 여기에 main.js 파일이 빠졌던 것이고 실행을 못했던 것이다. 다른 파일들도 하나도 안 들어가 있는 걸 보니 높은 확률로 빌드 실패가 뜬 것 같다. 어째서 빌드가 실패했는데 다음 스텝으로 넘어간 거지?
package.json 변경이력
아래처럼 해놨다. 내가 로컬에서 pnpm run build
하고 pnpm run start:prod
가 자꾸 안되길래 저렇게 바꿔놓은 채로 실행한 거다. 빌드 결과는 손도 안 댔는데 언제부터 빌드위치가 dist/src
로 바뀐거지?
- "start:prod": "node dist/main",
+ "start:prod": "node dist/src/main.js",
(CONCLUSION) tsconfig.json 쪽에 include 문제였다.
- "include": ["src/**/*.ts", "src/uploads/template.xlsx"]
+ "include": ["src/**/*.ts", "src/uploads/template.xlsx", "jest.config.ts"]
이걸 한 뒤로 dist 폴더구조가 바뀐 것이고 "jest.config.ts" 아이템을 제거하자 다시 dist 폴더 안에 컴파일 결과가 들어왔다. 원인이 뭐지?
🔍 원인 분석: include
에 jest.config.ts
추가가 dist 구조를 바꾼 이유
결론:
tsconfig.json
에서 include
에 "jest.config.ts"
를 추가했을 때, **TypeScript의 루트 디렉터리 감지(rootDir
설정)**가 달라졌기 때문이야.
💡 원리: rootDir
결정 방식
TypeScript는 tsconfig.json
에서 include
된 파일들의 공통 상위 디렉터리를 rootDir
로 설정하고, outDir
로 변환할 때 이 rootDir
을 기준으로 상대 경로를 유지해.
- 즉,
rootDir
이 다르면dist/
안에 빌드된 파일들의 구조가 달라짐. include
에 추가한 파일이 기존과 다른 위치에 있으면rootDir
이 예상과 다르게 설정될 수 있음.
⚠️ jest.config.ts
추가가 왜 문제였나?
-
원래는
include
가"src/**/*.ts"
였기 때문에 TypeScript는src/
를rootDir
로 인식하고dist/
안에main.js
등 관련 파일을 넣었음.src/ -> dist/ ├── main.ts -> ├── main.js ├── user/ -> ├── user/ ├── academy/ -> ├── academy/
-
하지만
jest.config.ts
는src/
가 아닌 **루트 경로(/
)**에 있었을 가능성이 높음.📁 프로젝트 루트 ├── src/ │ ├── main.ts │ ├── user/ │ ├── academy/ ├── jest.config.ts # ✅ include에 추가됨!
-
TypeScript가
include
된 모든 파일의 공통 상위 경로를rootDir
로 설정하는데,src/**/*.ts
만 포함되었을 때 →rootDir
은src/
jest.config.ts
를 포함했을 때 →rootDir
은 프로젝트 루트(/
)
-
rootDir
이/
로 확장되면서 TypeScript는 다음처럼 파일 구조를 유지하려고 함:📁 dist/ ├── src/ ✅ 원래 dist 안에 없던 `src/` 폴더가 추가됨 │ ├── main.js │ ├── user/ │ ├── academy/ ├── jest.config.js
→ 이 때문에
dist/main.js
가 없어지고dist/src/main.js
가 생긴 것!
🛠 해결 방법
✅ 해결책 1: jest.config.ts
를 include
에서 제거 (지금 해결한 방법)
✅ 해결책 2: tsconfig.json
에 rootDir
을 명시적으로 설정
{
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
}
}
- 이렇게 하면
include
가 변경되더라도 TypeScript가src/
를rootDir
로 고정하므로,dist/
안에src/
폴더가 생기지 않음.
✅ 해결책 3: exclude
에 jest.config.ts
추가
{
"exclude": ["jest.config.ts"]
}
include
에서jest.config.ts
를 제거하는 것과 동일한 효과.
📌 결론
✅ tsconfig.json
의 include
에 프로젝트 루트 경로의 파일을 추가하면 TypeScript의 rootDir
이 변경될 수 있음.
✅ rootDir
이 변경되면 빌드 결과의 dist/
폴더 구조가 예상과 달라짐.
✅ 해결 방법: rootDir
을 명시적으로 src/
로 설정하거나 jest.config.ts
를 include
에서 제거. 🚀